/*
    RoleCall provides a web service that web applications can use to find 
    the roles associated with a user that has been authenticated via OpenID. 
    Also provides a web-based user interface to manage the user roles 
    associated with partner web applications.
    
    Development of this software was supported in part by the David and Lucile 
    Packard Foundation and by the the Office Of Naval Research (ONR) 
     
    Copyright (c) 2012, 
    Monterey Bay Aquarium Research Institute - MBARI ( www.mbari.org )
    Michael Godin ( mikegodin AT users DOT sourceforge DOT net )

    RoleCall is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    RoleCall is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Foobar (see the file COPYING.GPL).  If not, see 
    <http://www.gnu.org/licenses/>.  
*/

package org.mbari.rolecall.server;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.servlet.http.HttpServletRequest;

import org.mbari.rolecall.model.Status;
import org.mbari.rolecall.model.User;

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

public class RoleCallConsumer {

    protected static final BASE64Decoder base64Dec = new BASE64Decoder();

    protected static BASE64Encoder base64Enc = new BASE64Encoder();

    static protected final int bufferSize = 4096;

    static private HostnameVerifier hostnameVerifier;

    static {
        // Create a our custom trust manager
        TrustManager[] trustAllCerts = new TrustManager[] { new RoleCallX509TrustManager() };
        // Install the all-trusting trust manager
        try {
            SSLContext sc = SSLContext.getInstance("SSL");
            sc.init(null, trustAllCerts, new java.security.SecureRandom());
            HttpsURLConnection
                    .setDefaultSSLSocketFactory(sc.getSocketFactory());
        } catch (Exception e) {
        }
        // Create our custom host name verifier
        hostnameVerifier = new RoleCallHostNameVerifier();
    }

    static protected void applyHostnameVerifier(final URLConnection conn) {
        if (null != conn && conn instanceof HttpsURLConnection) {
            ((HttpsURLConnection) conn).setHostnameVerifier(hostnameVerifier);
        }
    };

    public synchronized static void assignRoles(HttpServletRequest request,
            User user, PersistentPropertyStore propertyStore) {
        try {
            // System.out.println("*** In assignRoles!");
            String publicKey = propertyStore.get("roleCallPublicKey", null);
            String publicModulus = propertyStore.get("roleCallPublicModulus",
                    null);
            String privateExponent = propertyStore.get(
                    "roleCallPrivateExponent", null);
            String roleCallUrl = propertyStore.get("roleCallUrl", null);

            String roleCallRelayId = propertyStore.get("roleCallRelayId", null);

            StringBuilder getRoleUrlStr = new StringBuilder(roleCallUrl);
            getRoleUrlStr.append("/getRoles?email=").append(
                    URLEncoder.encode(user.getEmail(), "UTF-8"));
            getRoleUrlStr.append("&openId=").append(
                    URLEncoder.encode(user.getOpenId(), "UTF-8"));
            getRoleUrlStr.append("&roleCallRelayId=").append(
                    URLEncoder.encode(roleCallRelayId, "UTF-8"));
            URL getRoleUrl;
            try {
                getRoleUrl = new URI(getRoleUrlStr.toString()).toURL();
            } catch (URISyntaxException e) {
                throw new IOException("Invalid URL: " + getRoleUrlStr, e);
            }

            StringBuilder encrypted = new StringBuilder();
            readURLIntoBuffer(encrypted, getRoleUrl);

            String encryptedStr = encrypted.toString();
            if (encryptedStr.startsWith("Error")) {
                throw new IOException(roleCallUrl + " responded: "
                        + encryptedStr);
            }
            String rolesDecrypted = decrypt(publicModulus, privateExponent,
                    encryptedStr);

            int firstComma = rolesDecrypted.indexOf(",");

            if (firstComma >= 0) {
                user.setRoles(rolesDecrypted.substring(firstComma + 1));
            }
            request.getSession().setAttribute("roleCallError", null);
        } catch (IOException ioe) {
            request.getSession()
                    .setAttribute("roleCallError", ioe.getMessage());
        }
    }

    public static boolean checkConfiguration(final HttpServletRequest req,
            PersistentPropertyStore propertyStore) {
        boolean configured = false;
        try {
            String publicKey = propertyStore.get("roleCallPublicKey", null);
            if (null == publicKey) {
                publicKey = makeRSAKeys(propertyStore, 1024);
            }
            String roleCallRelayId = propertyStore.get("roleCallRelayId", null);
            configured = null != roleCallRelayId;
            if (!configured) {
                String roleCallRelayTitle = req
                        .getParameter("roleCallRelayTitle");
                if (null == roleCallRelayTitle || roleCallRelayTitle.isEmpty()) {
                    roleCallRelayTitle = (String) req
                            .getAttribute("roleCallRelayTitle");
                    if (null == roleCallRelayTitle
                            || roleCallRelayTitle.isEmpty()) {
                        return false;
                    }
                }
                propertyStore.set("roleCallRelayTitle", roleCallRelayTitle);

                String roleCallUrl = req.getParameter("roleCallUrl");
                if (null == roleCallUrl || roleCallUrl.isEmpty()) {
                    roleCallUrl = (String) req.getAttribute("roleCallUrl");
                    if (null == roleCallUrl || roleCallUrl.isEmpty()) {
                        return false;
                    }
                }
                propertyStore.set("roleCallUrl", roleCallUrl);

                try {
                    new URI(roleCallUrl).toURL();
                } catch (Exception e) {
                    throw new IOException("Invalid URL: " + roleCallUrl, e);
                }

                StringBuilder registerUrlStr = new StringBuilder(roleCallUrl);

                registerUrlStr.append("/register?title=").append(
                        URLEncoder.encode(roleCallRelayTitle, "UTF-8"));
                registerUrlStr.append("&adminRole=").append(
                        URLEncoder.encode(propertyStore.get("adminRole", ""),
                                "UTF-8"));
                registerUrlStr.append("&roles=").append(
                        URLEncoder.encode(propertyStore.get("roles", ""),
                                "UTF-8"));
                registerUrlStr.append("&publicKey=").append(
                        URLEncoder.encode(publicKey, "UTF-8"));

                StringBuilder idResponse = new StringBuilder();
                URL registerUrl;
                try {
                    registerUrl = new URI(registerUrlStr.toString()).toURL();
                } catch (Exception e) {
                    throw new IOException("Invalid URL: " + registerUrlStr, e);
                }

                if (!readURLIntoBuffer(idResponse, registerUrl)) {
                    throw new IOException(
                            "Nothing returned from attempt to register with "
                                    + roleCallUrl);
                }
                roleCallRelayId = idResponse.toString();

                try {
                    Integer.valueOf(roleCallRelayId);
                    propertyStore.set("roleCallRelayId", roleCallRelayId);
                    configured = true;
                } catch (NumberFormatException nfe) {
                    throw new IOException("Unexpected register response from "
                            + roleCallUrl + ": " + nfe);
                }

            }
        } catch (IOException ioe) {
            req.getSession().setAttribute("roleCallError", ioe.getMessage());
            return false;
        }
        req.getSession().setAttribute("roleCallError", null);
        return configured;
    }

    public static String decrypt(String publicModulus, String privateExponent,
            final String encrypted) throws IOException {
        BigInteger rsaModulus = new BigInteger(1,
                base64Dec.decodeBuffer(publicModulus));
        BigInteger rsaPrivExp = new BigInteger(1,
                base64Dec.decodeBuffer(privateExponent));
        byte[] encryptedBytes = base64Dec.decodeBuffer(encrypted);
        BigInteger biEncoded = new BigInteger(1, encryptedBytes);
        BigInteger biDecoded = biEncoded.modPow(rsaPrivExp, rsaModulus);
        int nonZeroCount = 0;
        byte[] decryptedBytes = biDecoded.toByteArray();
        int offset = decryptedBytes.length > encryptedBytes.length ? 1 : 0;
        for (int i = 0; i < decryptedBytes.length - offset; ++i) {
            if (decryptedBytes[offset + i] != 0)
                ++nonZeroCount;
            else
                break;
        }
        String decrypted = null;
        try {
            decrypted = new String(decryptedBytes, offset, nonZeroCount, "UTF8");
        } catch (UnsupportedEncodingException uee) {
            throw new IOException("No UTF8 encoding?!?", uee);
        }
        return decrypted;
    }

    public static String getSessionOpenIdError(HttpServletRequest req) {
        return (String) req.getSession().getAttribute(
                "openid_servlet_filter_msg");
    }

    static public String getSessionRoleCallError(HttpServletRequest req) {
        return (String) req.getSession().getAttribute("roleCallError");
    }

    static public User getSessionUser(HttpServletRequest req) {
        return (User) req.getSession().getAttribute("user");
    }

    static public Status getStatus(HttpServletRequest req) {
        Status status = new Status();
        status.setUser(RoleCallConsumer.getSessionUser(req));
        status.setSessionId(req.getSession().getId());
        status.setOpenIdError(RoleCallConsumer.getSessionOpenIdError(req));
        status.setRoleCallError(RoleCallConsumer.getSessionRoleCallError(req));
        return status;
    }

    public static String makeRSAKeys(PersistentPropertyStore propertyStore,
            final int length) {

        KeyPairGenerator kpg;
        try {
            kpg = KeyPairGenerator.getInstance("RSA");
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        }
        kpg.initialize(length);
        KeyPair kp = kpg.genKeyPair();

        RSAPublicKeySpec pub;
        RSAPrivateKeySpec priv;
        BigInteger modulus;
        BigInteger pubExp;
        BigInteger privExp;
        KeyFactory fact;
        try {
            fact = KeyFactory.getInstance("RSA");
            pub = fact.getKeySpec(kp.getPublic(), RSAPublicKeySpec.class);
            modulus = pub.getModulus();
            pubExp = pub.getPublicExponent();
            priv = fact.getKeySpec(kp.getPrivate(), RSAPrivateKeySpec.class);
            privExp = priv.getPrivateExponent();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        } catch (InvalidKeySpecException e) {
            e.printStackTrace();
            return null;
        }

        // System.out.println("mod=" + modulus.toString(16));
        final byte[] modBytes = modulus.toByteArray();
        // System.out.println("modBytes=" + new
        // String(Hex.encodeHex(modBytes)));
        int modBitLen = modulus.bitLength();
        int modByteOffset = (modBytes.length > (modBitLen + 7) / 8) ? 1 : 0;
        int modByteLen = modBytes.length - modByteOffset;

        // System.out.println("exp=" + pubExp.toString(16));
        final byte[] expBytes = pubExp.toByteArray();
        // System.out.println("expBytes=" + new
        // String(Hex.encodeHex(expBytes)));
        int expBitLen = pubExp.bitLength();
        int expByteOffset = (expBytes.length > (expBitLen + 7) / 8) ? 1 : 0;
        int expByteLen = expBytes.length - expByteOffset;

        final byte[] pubBytes = new byte[modByteLen + expByteLen + 4];
        pubBytes[0] = (byte) ((modBitLen >> 8) & 0x000000FF);
        pubBytes[1] = (byte) ((modBitLen) & 0x000000FF);
        System.arraycopy(modBytes, modByteOffset, pubBytes, 2, modByteLen);
        pubBytes[modByteLen + 2] = (byte) ((expBitLen >> 8) & 0x000000FF);
        pubBytes[modByteLen + 3] = (byte) ((expBitLen) & 0x000000FF);
        System.arraycopy(expBytes, expByteOffset, pubBytes, modByteLen + 4,
                expByteLen);
        // System.out.println("pubBytes=" + new
        // String(Hex.encodeHex(pubBytes)));

        String publicKey = base64Enc.encode(pubBytes).replace("\n", "")
                .replace("\r", "");

        propertyStore.set("roleCallPublicKey", publicKey);
        propertyStore.set("roleCallPublicModulus", base64Enc.encode(modBytes)
                .replace("\n", "").replace("\r", ""));
        propertyStore.set("roleCallPrivateExponent",
                base64Enc.encode(privExp.toByteArray()));

        return publicKey;

    }

    protected static boolean readURLIntoBuffer(
            final StringBuilder fileContents, final URL urlToTry)
            throws IOException {

        InputStream inputStream = null;
        URLConnection conn = urlToTry.openConnection();
        applyHostnameVerifier(conn);
        conn.setUseCaches(false);
        conn.setAllowUserInteraction(false);
        conn.setConnectTimeout(5000);
        conn.setReadTimeout(5000);

        try {
            if (null != conn) {
                inputStream = conn.getInputStream();
            }
            final byte[] webBuffer = new byte[bufferSize];
            int bytesRead = bufferSize;
            try {
                while (bytesRead >= 0) {
                    bytesRead = inputStream.read(webBuffer);
                    if (bytesRead >= 0) {
                        fileContents
                                .append(new String(webBuffer, 0, bytesRead));
                    }
                }
            } catch (IOException ioe) {
                throw new IOException("Can not read from " + urlToTry, ioe);
            } finally {
                inputStream.close();
                if (null != conn && conn instanceof HttpURLConnection) {
                    ((HttpURLConnection) conn).disconnect();
                }
            }
        } catch (IOException ioe) {
            throw new IOException("Can not open " + urlToTry, ioe);
        }

        return fileContents.length() > 0;
    }

}
